/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ibm.corebanking.idmf.dubbo.server.annotation;

import static com.alibaba.spring.util.AnnotatedBeanDefinitionRegistryUtils.registerBeans;
import static com.alibaba.spring.util.AnnotationUtils.getAttributes;
import static com.alibaba.spring.util.ObjectUtils.of;
import static java.util.Arrays.asList;
import static org.apache.dubbo.config.spring.beans.factory.annotation.ServiceBeanNameBuilder.create;
import static org.apache.dubbo.config.spring.util.DubboAnnotationUtils.resolveServiceInterfaceClass;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
import static org.springframework.context.annotation.AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
import static org.springframework.core.annotation.AnnotationUtils.getAnnotationAttributes;
import static org.springframework.util.ClassUtils.resolveClassName;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ArrayUtils;
import org.apache.dubbo.config.MethodConfig;
import org.apache.dubbo.config.annotation.Method;
import org.apache.dubbo.config.spring.ServiceBean;
import org.apache.dubbo.config.spring.beans.factory.annotation.ServiceBeanNameBuilder;
import org.apache.dubbo.config.spring.context.DubboBootstrapApplicationListener;
import org.apache.dubbo.config.spring.context.annotation.DubboClassPathBeanDefinitionScanner;
import org.apache.dubbo.config.spring.schema.AnnotationBeanDefinitionParser;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import com.ibm.corebanking.idmf.dubbo.server.MultiServiceCodeSpliter;

/**
 * 处理 {@link Service @IdmfDubboService} annotated classes. 按配置，生成单服务或多服务实例
 *
 * @see AnnotationBeanDefinitionParser
 * @see BeanDefinitionRegistryPostProcessor
 */
public class IdmfDubboServiceClassPostProcessor
		implements BeanDefinitionRegistryPostProcessor, EnvironmentAware, ResourceLoaderAware, BeanClassLoaderAware,
		ApplicationContextAware {

	private final static List<Class<? extends Annotation>> serviceAnnotationTypes = asList(
			IdmfDubboService.class);

	private final Logger logger = LoggerFactory.getLogger(getClass());

	protected final Set<String> packagesToScan;

	private Environment environment;

	private ResourceLoader resourceLoader;

	private ClassLoader classLoader;

	public IdmfDubboServiceClassPostProcessor(String... packagesToScan) {
		this(asList(packagesToScan));
	}

	public IdmfDubboServiceClassPostProcessor(Collection<String> packagesToScan) {
		this(new LinkedHashSet<>(packagesToScan));
	}

	public IdmfDubboServiceClassPostProcessor(Set<String> packagesToScan) {
		this.packagesToScan = packagesToScan;
	}

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

		// @since 2.7.5
		registerBeans(registry, DubboBootstrapApplicationListener.class);

		Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

		if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
			registerServiceBeans(resolvedPackagesToScan, registry);
		} else {
			if (logger.isWarnEnabled()) {
				logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
			}
		}

	}

	/**
	 * Registers Beans whose classes was annotated {@link Service}
	 *
	 * @param packagesToScan The base packages to scan
	 * @param registry       {@link BeanDefinitionRegistry}
	 */
	private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

		DubboClassPathBeanDefinitionScanner scanner = new DubboClassPathBeanDefinitionScanner(registry, environment,
				resourceLoader);

		BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);

		scanner.setBeanNameGenerator(beanNameGenerator);

		// refactor @since 2.7.7
		serviceAnnotationTypes.forEach(annotationType -> {
			scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
		});

		for (String packageToScan : packagesToScan) {

			// Registers @Service Bean first
			scanner.scan(packageToScan);

			// Finds all BeanDefinitionHolders of @Service whether @ComponentScan scans or
			// not.
			Set<BeanDefinitionHolder> beanDefinitionHolders = findServiceBeanDefinitionHolders(scanner, packageToScan,
					registry, beanNameGenerator);

			if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

				for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
					registerServiceBean(beanDefinitionHolder, registry, scanner);
				}

				if (logger.isInfoEnabled()) {
					logger.info(beanDefinitionHolders.size() + " annotated Dubbo's @Service Components { "
							+ beanDefinitionHolders + " } were scanned under package[" + packageToScan + "]");
				}

			} else {

				if (logger.isWarnEnabled()) {
					logger.warn("No Spring Bean annotating Dubbo's @Service was found under package[" + packageToScan
							+ "]");
				}

			}

		}

	}

	/**
	 * It'd better to use BeanNameGenerator instance that should reference
	 * {@link ConfigurationClassPostProcessor#componentScanBeanNameGenerator}, thus
	 * it maybe a potential problem on bean name generation.
	 *
	 * @param registry {@link BeanDefinitionRegistry}
	 * @return {@link BeanNameGenerator} instance
	 * @see SingletonBeanRegistry
	 * @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
	 * @see ConfigurationClassPostProcessor#processConfigBeanDefinitions
	 * @since 2.5.8
	 */
	private BeanNameGenerator resolveBeanNameGenerator(BeanDefinitionRegistry registry) {

		BeanNameGenerator beanNameGenerator = null;

		if (registry instanceof SingletonBeanRegistry) {
			SingletonBeanRegistry singletonBeanRegistry = SingletonBeanRegistry.class.cast(registry);
			beanNameGenerator = (BeanNameGenerator) singletonBeanRegistry
					.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
		}

		if (beanNameGenerator == null) {

			if (logger.isInfoEnabled()) {

				logger.info("BeanNameGenerator bean can't be found in BeanFactory with name ["
						+ CONFIGURATION_BEAN_NAME_GENERATOR + "]");
				logger.info("BeanNameGenerator will be a instance of " + AnnotationBeanNameGenerator.class.getName()
						+ " , it maybe a potential problem on bean name generation.");
			}

			beanNameGenerator = new AnnotationBeanNameGenerator();

		}

		return beanNameGenerator;

	}

	/**
	 * Finds a {@link Set} of {@link BeanDefinitionHolder BeanDefinitionHolders}
	 * whose bean type annotated {@link Service} Annotation.
	 *
	 * @param scanner       {@link ClassPathBeanDefinitionScanner}
	 * @param packageToScan pachage to scan
	 * @param registry      {@link BeanDefinitionRegistry}
	 * @return non-null
	 * @since 2.5.8
	 */
	private Set<BeanDefinitionHolder> findServiceBeanDefinitionHolders(ClassPathBeanDefinitionScanner scanner,
			String packageToScan, BeanDefinitionRegistry registry, BeanNameGenerator beanNameGenerator) {

		Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(packageToScan);

		Set<BeanDefinitionHolder> beanDefinitionHolders = new LinkedHashSet<>(beanDefinitions.size());

		for (BeanDefinition beanDefinition : beanDefinitions) {

			String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);
			BeanDefinitionHolder beanDefinitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
			beanDefinitionHolders.add(beanDefinitionHolder);

		}

		return beanDefinitionHolders;

	}

	/**
	 * Registers {@link ServiceBean} from new annotated {@link Service}
	 * {@link BeanDefinition}
	 *
	 * @param beanDefinitionHolder
	 * @param registry
	 * @param scanner
	 * @see ServiceBean
	 * @see BeanDefinition
	 */
	private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry,
			DubboClassPathBeanDefinitionScanner scanner) {

		Class<?> beanClass = resolveClass(beanDefinitionHolder);

		Annotation service = findServiceAnnotation(beanClass);
		IdmfDubboService[] multiServAnnots = beanClass.getAnnotationsByType(IdmfDubboService.class);
		if (multiServAnnots.length != 1) {
			throw new RuntimeException("IdmfDubboService must be defined");
		}
		Map<String, Object> attrs = getAnnotationAttributes(multiServAnnots[0]);
		Class<MultiServiceCodeSpliter> serviceCodeSpliter = (Class) attrs.get("serviceCodeSpliter");
		String[] servIds;
		if (MultiServiceCodeSpliter.class != serviceCodeSpliter) {
			MultiServiceCodeSpliter spliter = this.applicationContext.getBean(serviceCodeSpliter);
			servIds = spliter.split();
		} else {
			servIds = new String[] { null };
		}
		for (String servId : servIds) { // By Zhouwei multi service
			/**
			 * The {@link AnnotationAttributes} of @IdmfDubboService annotation
			 */
			AnnotationAttributes serviceAnnotationAttributes = getAnnotationAttributes(service, false, false);
			String group = serviceAnnotationAttributes.getString("group");
			StringBuilder grpBld = new StringBuilder();
			if (group != null && group.length() > 0) {
				grpBld.append(group);
			}
			if (servId != null && servId.length() > 0) {
				if (grpBld.length() > 0) {
					grpBld.append("-");
				}
				grpBld.append(servId);
			}
			serviceAnnotationAttributes.put("group", grpBld.toString());
			Class<?> interfaceClass = resolveServiceInterfaceClass(serviceAnnotationAttributes, beanClass);

			String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

			AbstractBeanDefinition serviceBeanDefinition = buildServiceBeanDefinition(service,
					serviceAnnotationAttributes, interfaceClass, annotatedServiceBeanName);

			// ServiceBean Bean name
			String beanName = generateServiceBeanName(serviceAnnotationAttributes, interfaceClass);

			if (scanner.checkCandidate(beanName, serviceBeanDefinition)) { // check duplicated candidate bean
				registry.registerBeanDefinition(beanName, serviceBeanDefinition);

				if (logger.isInfoEnabled()) {
					logger.info("The BeanDefinition[" + serviceBeanDefinition
							+ "] of ServiceBean has been registered with name : " + beanName);
				}

			} else {

				if (logger.isWarnEnabled()) {
					logger.warn("The Duplicated BeanDefinition[" + serviceBeanDefinition
							+ "] of ServiceBean[ bean name : " + beanName
							+ "] was be found , Did @DubboComponentScan scan to same package in many times?");
				}

			}
		}
	}

	/**
	 * Find the {@link Annotation annotation} of @Service
	 *
	 * @param beanClass the {@link Class class} of Bean
	 * @return <code>null</code> if not found
	 * @since 2.7.3
	 */
	private Annotation findServiceAnnotation(Class<?> beanClass) {
		return serviceAnnotationTypes.stream().map(annotationType -> findMergedAnnotation(beanClass, annotationType))
				.filter(Objects::nonNull).findFirst().orElse(null);
	}

	/**
	 * Generates the bean name of {@link ServiceBean}
	 *
	 * @param serviceAnnotationAttributes
	 * @param interfaceClass              the class of interface annotated
	 *                                    {@link Service}
	 * @return ServiceBean@interfaceClassName#annotatedServiceBeanName
	 * @since 2.7.3
	 */
	private String generateServiceBeanName(AnnotationAttributes serviceAnnotationAttributes, Class<?> interfaceClass) {
		ServiceBeanNameBuilder builder = create(interfaceClass, environment)
				.group(serviceAnnotationAttributes.getString("group"))
				.version(serviceAnnotationAttributes.getString("version"));
		return builder.build();
	}

	private Class<?> resolveClass(BeanDefinitionHolder beanDefinitionHolder) {

		BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();

		return resolveClass(beanDefinition);

	}

	private Class<?> resolveClass(BeanDefinition beanDefinition) {

		String beanClassName = beanDefinition.getBeanClassName();

		return resolveClassName(beanClassName, classLoader);

	}

	private Set<String> resolvePackagesToScan(Set<String> packagesToScan) {
		Set<String> resolvedPackagesToScan = new LinkedHashSet<String>(packagesToScan.size());
		for (String packageToScan : packagesToScan) {
			if (StringUtils.hasText(packageToScan)) {
				String resolvedPackageToScan = environment.resolvePlaceholders(packageToScan.trim());
				resolvedPackagesToScan.add(resolvedPackageToScan);
			}
		}
		return resolvedPackagesToScan;
	}

	/**
	 * Build the {@link AbstractBeanDefinition Bean Definition}
	 *
	 * @param serviceAnnotation
	 * @param serviceAnnotationAttributes
	 * @param interfaceClass
	 * @param annotatedServiceBeanName
	 * @return
	 * @since 2.7.3
	 */
	private AbstractBeanDefinition buildServiceBeanDefinition(Annotation serviceAnnotation,
			AnnotationAttributes serviceAnnotationAttributes, Class<?> interfaceClass,
			String annotatedServiceBeanName) {

		BeanDefinitionBuilder builder = rootBeanDefinition(ServiceBean.class);

		AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();

		MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();

		String[] ignoreAttributeNames = of("provider", "monitor", "application", "module", "registry", "protocol",
				"interface", "interfaceName", "parameters", "group", "serviceCodeSpliter");

		propertyValues.addPropertyValues(
				new AnnotationPropertyValuesAdapter(serviceAnnotation, environment, ignoreAttributeNames));

		// By Zhouwei
		propertyValues.add("group", serviceAnnotationAttributes.getString("group"));

		// References "ref" property to annotated-@Service Bean
		addPropertyReference(builder, "ref", annotatedServiceBeanName);
		// Set interface
		builder.addPropertyValue("interface", interfaceClass.getName());
		// Convert parameters into map
		builder.addPropertyValue("parameters",
				convertParameters(serviceAnnotationAttributes.getStringArray("parameters")));
		// Add methods parameters
		List<MethodConfig> methodConfigs = convertMethodConfigs(serviceAnnotationAttributes.get("methods"));
		if (!methodConfigs.isEmpty()) {
			builder.addPropertyValue("methods", methodConfigs);
		}

		/**
		 * Add {@link org.apache.dubbo.config.ProviderConfig} Bean reference
		 */
		String providerConfigBeanName = serviceAnnotationAttributes.getString("provider");
		if (StringUtils.hasText(providerConfigBeanName)) {
			addPropertyReference(builder, "provider", providerConfigBeanName);
		}

		/**
		 * Add {@link org.apache.dubbo.config.MonitorConfig} Bean reference
		 */
		String monitorConfigBeanName = serviceAnnotationAttributes.getString("monitor");
		if (StringUtils.hasText(monitorConfigBeanName)) {
			addPropertyReference(builder, "monitor", monitorConfigBeanName);
		}

		/**
		 * Add {@link org.apache.dubbo.config.ApplicationConfig} Bean reference
		 */
		String applicationConfigBeanName = serviceAnnotationAttributes.getString("application");
		if (StringUtils.hasText(applicationConfigBeanName)) {
			addPropertyReference(builder, "application", applicationConfigBeanName);
		}

		/**
		 * Add {@link org.apache.dubbo.config.ModuleConfig} Bean reference
		 */
		String moduleConfigBeanName = serviceAnnotationAttributes.getString("module");
		if (StringUtils.hasText(moduleConfigBeanName)) {
			addPropertyReference(builder, "module", moduleConfigBeanName);
		}

		/**
		 * Add {@link org.apache.dubbo.config.RegistryConfig} Bean reference
		 */
		String[] registryConfigBeanNames = serviceAnnotationAttributes.getStringArray("registry");

		List<RuntimeBeanReference> registryRuntimeBeanReferences = toRuntimeBeanReferences(registryConfigBeanNames);

		if (!registryRuntimeBeanReferences.isEmpty()) {
			builder.addPropertyValue("registries", registryRuntimeBeanReferences);
		}

		/**
		 * Add {@link org.apache.dubbo.config.ProtocolConfig} Bean reference
		 */
		String[] protocolConfigBeanNames = serviceAnnotationAttributes.getStringArray("protocol");

		List<RuntimeBeanReference> protocolRuntimeBeanReferences = toRuntimeBeanReferences(protocolConfigBeanNames);

		if (!protocolRuntimeBeanReferences.isEmpty()) {
			builder.addPropertyValue("protocols", protocolRuntimeBeanReferences);
		}

		return builder.getBeanDefinition();

	}

	private List convertMethodConfigs(Object methodsAnnotation) {
		if (methodsAnnotation == null) {
			return Collections.EMPTY_LIST;
		}
		return MethodConfig.constructMethodConfig((Method[]) methodsAnnotation);
	}

	private ManagedList<RuntimeBeanReference> toRuntimeBeanReferences(String... beanNames) {

		ManagedList<RuntimeBeanReference> runtimeBeanReferences = new ManagedList<>();

		if (!ObjectUtils.isEmpty(beanNames)) {

			for (String beanName : beanNames) {

				String resolvedBeanName = environment.resolvePlaceholders(beanName);

				runtimeBeanReferences.add(new RuntimeBeanReference(resolvedBeanName));
			}

		}

		return runtimeBeanReferences;

	}

	private void addPropertyReference(BeanDefinitionBuilder builder, String propertyName, String beanName) {
		String resolvedBeanName = environment.resolvePlaceholders(beanName);
		builder.addPropertyReference(propertyName, resolvedBeanName);
	}

	private Map<String, String> convertParameters(String[] parameters) {
		if (ArrayUtils.isEmpty(parameters)) {
			return null;
		}

		if (parameters.length % 2 != 0) {
			throw new IllegalArgumentException("parameter attribute must be paired with key followed by value");
		}

		Map<String, String> map = new HashMap<>();
		for (int i = 0; i < parameters.length; i += 2) {
			map.put(parameters[i], parameters[i + 1]);
		}
		return map;
	}

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

	}

	@Override
	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	ApplicationContext applicationContext;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;

	}

	class AnnotationPropertyValuesAdapter implements PropertyValues {

		private final PropertyValues delegate;

		/**
		 * @param attributes
		 * @param propertyResolver
		 * @param ignoreAttributeNames
		 * @since 2.7.3
		 */
		public AnnotationPropertyValuesAdapter(Map<String, Object> attributes, PropertyResolver propertyResolver,
				String... ignoreAttributeNames) {
			this.delegate = new MutablePropertyValues(
					getAttributes(attributes, propertyResolver, ignoreAttributeNames));
		}

		public AnnotationPropertyValuesAdapter(Annotation annotation, PropertyResolver propertyResolver,
				boolean ignoreDefaultValue, String... ignoreAttributeNames) {
			this.delegate = new MutablePropertyValues(
					getAttributes(annotation, propertyResolver, ignoreDefaultValue, ignoreAttributeNames));
		}

		public AnnotationPropertyValuesAdapter(Annotation annotation, PropertyResolver propertyResolver,
				String... ignoreAttributeNames) {
			this(annotation, propertyResolver, true, ignoreAttributeNames);
		}

		@Override
		public PropertyValue[] getPropertyValues() {
			return delegate.getPropertyValues();
		}

		@Override
		public PropertyValue getPropertyValue(String propertyName) {
			return delegate.getPropertyValue(propertyName);
		}

		@Override
		public PropertyValues changesSince(PropertyValues old) {
			return delegate.changesSince(old);
		}

		@Override
		public boolean contains(String propertyName) {
			return delegate.contains(propertyName);
		}

		@Override
		public boolean isEmpty() {
			return delegate.isEmpty();
		}
	}

}
